import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import {
Alert,
Image,
KeyboardAvoidingView,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
TextInput,
View,
} from 'react-native';
import * as ImagePicker from 'expo-image-picker';
import DateTimePicker from '@react-native-community/datetimepicker';
import { ResizeMode, Video } from 'expo-av';
import { useLocalSearchParams, useRouter } from 'expo-router';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { useNavigation } from '@react-navigation/native';
import { ThemedButton } from '@/components/themed-button';
import { IconButton } from '@/components/icon-button';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { ZoomImageModal } from '@/components/zoom-image-modal';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useTranslation } from '@/localization/i18n';
import { dbPromise, initCoreTables } from '@/services/db';
type FieldRow = {
id: number;
name: string | null;
};
type CropRow = {
id: number;
crop_name: string | null;
};
type HarvestRow = {
id: number;
field_id: number | null;
crop_id: number | null;
harvested_at: string | null;
quantity: number | null;
unit: string | null;
notes: string | null;
photo_uri: string | null;
};
type MediaRow = {
uri: string | null;
};
export default function HarvestDetailScreen() {
const { t } = useTranslation();
const router = useRouter();
const navigation = useNavigation();
const { id } = useLocalSearchParams<{ id?: string | string[] }>();
const harvestId = Number(Array.isArray(id) ? id[0] : id);
const theme = useColorScheme() ?? 'light';
const palette = Colors[theme];
useLayoutEffect(() => {
navigation.setOptions({
headerBackTitleVisible: false,
headerBackTitle: '',
headerBackTitleStyle: { display: 'none' },
headerLeft: () => (
router.back()} style={{ paddingHorizontal: 8 }}>
),
});
}, [navigation, palette.text, router]);
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState('');
const [fields, setFields] = useState([]);
const [crops, setCrops] = useState([]);
const [fieldModalOpen, setFieldModalOpen] = useState(false);
const [cropModalOpen, setCropModalOpen] = useState(false);
const [selectedFieldId, setSelectedFieldId] = useState(null);
const [selectedCropId, setSelectedCropId] = useState(null);
const [harvestDate, setHarvestDate] = useState('');
const [showHarvestPicker, setShowHarvestPicker] = useState(false);
const [quantity, setQuantity] = useState('');
const [unit, setUnit] = useState('');
const [notes, setNotes] = useState('');
const [mediaUris, setMediaUris] = useState([]);
const [activeUri, setActiveUri] = useState(null);
const [errors, setErrors] = useState<{ field?: string; crop?: string; quantity?: string }>({});
const [zoomUri, setZoomUri] = useState(null);
const [saving, setSaving] = useState(false);
const [showSaved, setShowSaved] = useState(false);
const unitPresets = [
{ key: 'kg', label: 'kg' },
{ key: 'g', label: 'g' },
{ key: 'ton', label: 'ton' },
{ key: 'pcs', label: 'pcs' },
];
useEffect(() => {
let isActive = true;
async function loadHarvest() {
try {
await initCoreTables();
const db = await dbPromise;
const fieldRows = await db.getAllAsync('SELECT id, name FROM fields ORDER BY name ASC;');
const cropRows = await db.getAllAsync('SELECT id, crop_name FROM crops ORDER BY crop_name ASC;');
const rows = await db.getAllAsync(
'SELECT id, field_id, crop_id, harvested_at, quantity, unit, notes, photo_uri FROM harvests WHERE id = ? LIMIT 1;',
harvestId
);
if (!isActive) return;
setFields(fieldRows);
setCrops(cropRows);
const harvest = rows[0];
if (!harvest) {
setStatus(t('harvests.empty'));
setLoading(false);
return;
}
setSelectedFieldId(harvest.field_id ?? null);
setSelectedCropId(harvest.crop_id ?? null);
setHarvestDate(harvest.harvested_at ?? '');
setQuantity(harvest.quantity !== null ? String(harvest.quantity) : '');
setUnit(harvest.unit ?? '');
setNotes(harvest.notes ?? '');
const mediaRows = await db.getAllAsync(
'SELECT uri FROM harvest_media WHERE harvest_id = ? ORDER BY created_at ASC;',
harvestId
);
const media = uniqueMediaUris([
...(mediaRows.map((row) => row.uri).filter(Boolean) as string[]),
...(normalizeMediaUri(harvest.photo_uri) ? [normalizeMediaUri(harvest.photo_uri) as string] : []),
]);
setMediaUris(media);
setActiveUri(media[0] ?? normalizeMediaUri(harvest.photo_uri));
} catch (error) {
if (isActive) setStatus(`Error: ${String(error)}`);
} finally {
if (isActive) setLoading(false);
}
}
loadHarvest();
return () => {
isActive = false;
};
}, [harvestId, t]);
const selectedField = useMemo(
() => fields.find((item) => item.id === selectedFieldId),
[fields, selectedFieldId]
);
const selectedCrop = useMemo(
() => crops.find((item) => item.id === selectedCropId),
[crops, selectedCropId]
);
const inputStyle = [
styles.input,
{
borderColor: palette.border,
backgroundColor: palette.input,
color: palette.text,
},
];
async function handleUpdate() {
const parsedQuantity = quantity.trim() ? Number(quantity) : null;
const nextErrors: { field?: string; crop?: string; quantity?: string } = {};
if (!selectedFieldId) nextErrors.field = t('harvests.fieldRequired');
if (!selectedCropId) nextErrors.crop = t('harvests.cropRequired');
if (quantity.trim() && !Number.isFinite(parsedQuantity)) {
nextErrors.quantity = t('harvests.quantityInvalid');
}
setErrors(nextErrors);
if (Object.keys(nextErrors).length > 0) return;
try {
setSaving(true);
const db = await dbPromise;
const now = new Date().toISOString();
const primaryUri = mediaUris[0] ?? normalizeMediaUri(activeUri);
await db.runAsync(
'UPDATE harvests SET field_id = ?, crop_id = ?, harvested_at = ?, quantity = ?, unit = ?, notes = ?, photo_uri = ? WHERE id = ?;',
selectedFieldId,
selectedCropId,
harvestDate || null,
parsedQuantity,
unit.trim() || null,
notes.trim() || null,
primaryUri ?? null,
harvestId
);
await db.runAsync('DELETE FROM harvest_media WHERE harvest_id = ?;', harvestId);
const mediaToInsert = uniqueMediaUris([
...mediaUris,
...(normalizeMediaUri(activeUri) ? [normalizeMediaUri(activeUri) as string] : []),
]);
for (const uri of mediaToInsert) {
await db.runAsync(
'INSERT INTO harvest_media (harvest_id, uri, media_type, created_at) VALUES (?, ?, ?, ?);',
harvestId,
uri,
isVideoUri(uri) ? 'video' : 'image',
now
);
}
setStatus(t('harvests.saved'));
setShowSaved(true);
setTimeout(() => {
setShowSaved(false);
setStatus('');
}, 1800);
} catch (error) {
setStatus(`Error: ${String(error)}`);
} finally {
setSaving(false);
}
}
function confirmDelete() {
Alert.alert(
t('harvests.deleteTitle'),
t('harvests.deleteMessage'),
[
{ text: t('harvests.cancel'), style: 'cancel' },
{
text: t('harvests.delete'),
style: 'destructive',
onPress: async () => {
const db = await dbPromise;
await db.runAsync('DELETE FROM harvest_media WHERE harvest_id = ?;', harvestId);
await db.runAsync('DELETE FROM harvests WHERE id = ?;', harvestId);
router.back();
},
},
]
);
}
if (loading) {
return (
{t('harvests.loading')}
);
}
return (
{t('harvests.edit')}
{status && !showSaved ? {status} : null}
{t('harvests.field')}
setFieldModalOpen(true)}
variant="secondary"
/>
{errors.field ? {errors.field} : null}
{t('harvests.crop')}
setCropModalOpen(true)}
variant="secondary"
/>
{errors.crop ? {errors.crop} : null}
{t('harvests.date')}
setShowHarvestPicker(true)} style={styles.dateInput}>
{harvestDate || t('harvests.datePlaceholder')}
{showHarvestPicker ? (
{
setShowHarvestPicker(false);
if (date) setHarvestDate(toDateOnly(date));
}}
/>
) : null}
{t('harvests.quantity')}
{
setQuantity(value);
if (errors.quantity) setErrors((prev) => ({ ...prev, quantity: undefined }));
}}
placeholder={t('harvests.quantityPlaceholder')}
placeholderTextColor={palette.placeholder}
style={inputStyle}
keyboardType="decimal-pad"
/>
{errors.quantity ? {errors.quantity} : null}
{t('harvests.unit')}
{unitPresets.map((preset) => {
const label = t(`units.${preset.key}`);
const isActive = unit === label || unit === preset.key;
return (
setUnit(label)}
style={[styles.unitChip, isActive ? styles.unitChipActive : null]}>
{label}
);
})}
{t('harvests.notes')}
{t('harvests.addMedia')}
{normalizeMediaUri(activeUri) ? (
isVideoUri(normalizeMediaUri(activeUri) as string) ? (
) : (
setZoomUri(normalizeMediaUri(activeUri) as string)}>
)
) : (
{t('harvests.noPhoto')}
)}
{mediaUris.length > 0 ? (
{mediaUris.map((uri) => (
setActiveUri(uri)}>
{isVideoUri(uri) ? (
▶
) : (
)}
{
event.stopPropagation();
setMediaUris((prev) => {
const next = prev.filter((item) => item !== uri);
setActiveUri((current) => (current === uri ? next[0] ?? null : current));
return next;
});
}}>
×
))}
) : null}
handlePickMedia((uris) => {
if (uris.length === 0) return;
setMediaUris((prev) => uniqueMediaUris([...prev, ...uris]));
setActiveUri((prev) => prev ?? uris[0]);
})
}
variant="secondary"
/>
handleTakeMedia((uri) => {
if (!uri) return;
setMediaUris((prev) => uniqueMediaUris([...prev, uri]));
setActiveUri((prev) => prev ?? uri);
})
}
variant="secondary"
/>
{showSaved ? {t('harvests.saved')} : null}
setFieldModalOpen(false)}>
{t('harvests.selectField')}
{fields.map((item) => (
{
setSelectedFieldId(item.id);
setFieldModalOpen(false);
}}>
{item.name || t('harvests.noField')}
))}
setCropModalOpen(false)}>
{t('harvests.selectCrop')}
{crops.map((item) => (
{
setSelectedCropId(item.id);
setCropModalOpen(false);
}}>
{item.crop_name || t('harvests.noCrop')}
))}
setZoomUri(null)} />
);
}
async function handlePickMedia(onAdd: (uris: string[]) => void) {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: getMediaTypes(),
quality: 1,
allowsMultipleSelection: true,
selectionLimit: 0,
});
if (result.canceled) return;
const uris = (result.assets ?? []).map((asset) => asset.uri).filter(Boolean) as string[];
if (uris.length === 0) return;
onAdd(uris);
}
async function handleTakeMedia(onAdd: (uri: string | null) => void) {
const permission = await ImagePicker.requestCameraPermissionsAsync();
if (!permission.granted) {
return;
}
const result = await ImagePicker.launchCameraAsync({
mediaTypes: getMediaTypes(),
quality: 1,
});
if (result.canceled) return;
const asset = result.assets[0];
onAdd(asset.uri);
}
function getMediaTypes() {
const mediaType = (ImagePicker as {
MediaType?: { Image?: unknown; Images?: unknown; Video?: unknown; Videos?: unknown };
}).MediaType;
const imageType = mediaType?.Image ?? mediaType?.Images;
const videoType = mediaType?.Video ?? mediaType?.Videos;
if (imageType && videoType) {
return [imageType, videoType];
}
return imageType ?? videoType ?? ['images', 'videos'];
}
function isVideoUri(uri: string) {
return /\.(mp4|mov|m4v|webm|avi|mkv)(\?.*)?$/i.test(uri);
}
function normalizeMediaUri(uri?: string | null) {
if (typeof uri !== 'string') return null;
const trimmed = uri.trim();
return trimmed ? trimmed : null;
}
function uniqueMediaUris(uris: string[]) {
const seen = new Set();
const result: string[] = [];
for (const uri of uris) {
if (!uri || seen.has(uri)) continue;
seen.add(uri);
result.push(uri);
}
return result;
}
function toDateOnly(date: Date) {
return date.toISOString().slice(0, 10);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
keyboardAvoid: {
flex: 1,
},
content: {
padding: 16,
gap: 10,
paddingBottom: 40,
},
input: {
borderRadius: 10,
borderWidth: 1,
paddingHorizontal: 12,
paddingVertical: 10,
fontSize: 15,
},
errorText: {
color: '#C0392B',
fontSize: 12,
},
dateInput: {
borderRadius: 10,
borderWidth: 1,
borderColor: '#B9B9B9',
paddingHorizontal: 12,
paddingVertical: 10,
},
dateValue: {
opacity: 0.7,
},
mediaPreview: {
width: '100%',
height: 220,
borderRadius: 12,
backgroundColor: '#1C1C1C',
},
photoRow: {
flexDirection: 'row',
gap: 8,
},
actions: {
marginTop: 12,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
gap: 10,
},
photoPlaceholder: {
opacity: 0.6,
},
unitRow: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
marginBottom: 8,
},
unitChip: {
borderRadius: 999,
borderWidth: 1,
borderColor: '#C6C6C6',
paddingHorizontal: 10,
paddingVertical: 4,
},
unitChipActive: {
borderColor: '#2F7D4F',
backgroundColor: '#E7F3EA',
},
unitText: {
fontSize: 12,
},
unitTextActive: {
fontSize: 12,
color: '#2F7D4F',
fontWeight: '600',
},
mediaStrip: {
marginTop: 6,
},
mediaChip: {
width: 72,
height: 72,
borderRadius: 10,
marginRight: 8,
overflow: 'hidden',
backgroundColor: '#E6E1D4',
alignItems: 'center',
justifyContent: 'center',
},
mediaThumb: {
width: '100%',
height: '100%',
},
videoThumb: {
width: '100%',
height: '100%',
backgroundColor: '#1C1C1C',
alignItems: 'center',
justifyContent: 'center',
},
videoThumbText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '700',
},
mediaRemove: {
position: 'absolute',
top: 4,
right: 4,
width: 18,
height: 18,
borderRadius: 9,
backgroundColor: 'rgba(0,0,0,0.6)',
alignItems: 'center',
justifyContent: 'center',
},
mediaRemoveText: {
color: '#FFFFFF',
fontSize: 12,
lineHeight: 14,
fontWeight: '700',
},
updateGroup: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
inlineToastText: {
fontWeight: '700',
fontSize: 12,
},
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.4)',
justifyContent: 'center',
padding: 24,
},
modalCard: {
borderRadius: 14,
backgroundColor: '#FFFFFF',
padding: 16,
gap: 10,
maxHeight: '80%',
},
modalList: {
maxHeight: 300,
},
modalItem: {
paddingVertical: 10,
},
});